//-------------------------------------------------------------------------------------------------
// Copyright (c) Bradford W. Mott and Flare Contributors
// North Carolina State University, Department of Computer Science
// The IntelliMedia Group
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
// OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
// SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
// OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//-------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using System.Xml;
using UnityEngine;

using Flare.Display;
using Flare.Events;
using Flare.Geom;
using Flare.UI;

namespace Flare.Text
{
    /// <summary>
    /// The TextField class represents dynamic text objects on the display list. It should
    /// be noted that the class is under development and has limited support at this time.
    /// Basic HTML formatted text can be used; however, rendering hasn't been optimized.
    /// User input/editting of the text field is not supported at this time.
    /// </summary>
    public class TextField : InteractiveObject
    {
        // TODO bwmott 2013-08-22: Add hitTest and proper cloning support

        internal string variableName { get; set; }
        internal TextFormat loadedTextFormat { get; set; }

        /// <summary>
        /// Indicates if automatic sizing and alignment of the text field should occur.
        /// Valid values are defined in the TextFieldAutoSize class.
        /// </summary>
        public string autoSize { get; set; }

        /// <summary>
        /// Indicates the background color of the text field as an RGB hexadecimal value.
        /// </summary>
        public bool background { get; set; }

        /// <summary>
        /// Indicates if a background should be drawn for the text field.
        /// </summary>
        public uint backgroundColor { get; set; }

        /// <summary>
        /// Indicates if a border should be drawn around the text field.
        /// </summary>
        public bool border { get; set; }

        /// <summary>
        /// Indicates the border color of the text field as an RGB hexadecimal value.
        /// </summary>
        public uint borderColor { get; set; }

        /// <summary>
        /// Indicates the insertion point where new text is entered.
        /// </summary>
        public int caretIndex { get; private set; }

        /// <summary>
        /// Indicates if the text field should be displayed as a password text field.
        /// </summary>
        public bool displayAsPassword { get; set; }

        /// <summary>
        /// Indicates the character to display in password text fields.
        /// </summary>
        public char passwordCharacter { get; set; }

        /// <summary>
        /// Indicates the text format to use for newly inserted text.
        /// </summary>
        public TextFormat defaultTextFormat { get; set; }

        /// <summary>
        /// Returns the number of characters in the text field.
        /// </summary>
        public int length
        { 
            get
            {
                if (!this.m_isHtml)
                {
                    return this.text.Length;
                }
                else
                {
                    // TODO bwmott 2014/08/22: Support length for HTML text fields
                    throw new System.InvalidOperationException("Accessing length is not " +
                        "currently supported for HTML text fields.");
                }
            }
        }

        public int maxChars { get; set; }
        public bool multiline { get; set; }

        public bool selectable { get; set; }

        public uint textColor { get; set; }
        public float textHeight { get; private set; }
        public float textWidth { get; private set; }

        /// <summary>
        /// Indicates if the text field should be word wrapped or not.
        /// </summary>
        public bool wordWrap { get; set; }

        /// <summary>
        /// Indicates the type of text field.
        /// </summary>
        public string type 
        {
            get
            {
                return this.m_type;
            }

            set
            {
                if (this.m_type != value)
                {
                    if (this.m_type == TextFieldType.INPUT)
                    {
                        // Remove event listeners since it will not be an input field anylonger
                        this.RemoveEventListener(KeyboardEvent.KEY_DOWN, OnKeyPress);
                        this.RemoveEventListener(FocusEvent.FOCUS_IN, OnFocusChange);
                        this.RemoveEventListener(FocusEvent.FOCUS_OUT, OnFocusChange);
                    }

                    if (value == TextFieldType.INPUT)
                    {
                        // Setup event listeners to handle focus changes and key presses
                        this.AddEventListener(KeyboardEvent.KEY_DOWN, OnKeyPress);
                        this.AddEventListener(FocusEvent.FOCUS_IN, OnFocusChange);
                        this.AddEventListener(FocusEvent.FOCUS_OUT, OnFocusChange);
                    }

                    this.m_type = value;
                }
            }
        }
        private string m_type;

        /// <summary>
        /// The html formatted text to display in the text field.
        /// </summary>
        public string htmlText
        {
            get
            {
                return this.m_htmlText;
            }

            set
            {
                this.m_htmlText = value;
                this.m_isHtml = true;
                this.caretIndex = 0;
                ParseTextRecords();
            }
        }
        private string m_htmlText;

        /// <summary>
        /// The current text in the text field.
        /// </summary>
        public string text
        {
            get
            {
                return this.m_text;
            }

            set
            {
                this.m_text = value;
                this.m_isHtml = false;
                this.m_dirty = true;
                this.caretIndex = 0;
            }
        }
        private string m_text;

        // Indicates if this is an HTML or plain text field.
        private bool m_isHtml;

        /// <summary>
        /// Width of the text field.
        /// </summary>
        public float width { get; set; }

        /// <summary>
        /// Height of the text field.
        /// </summary>
        public float height { get; set; }

        // public bool embedFonts { get; set; }
        // public int numLines { get; private set; }
        // public string restrict { get; set; }
        // public int selectionBeginIndex { get; private set; }
        // public int selectionEndIndex { get; private set; }

        // Indicates if the text field is currently in focus or not.
        private bool inFocus { get; set; }

        public override bool tabEnabled
        {
            get
            {
                if (this.m_tabEnabledExplicitlySet)
                {
                    return base.tabEnabled;
                }
                else
                {
                    // TabEnabled hasn't been explicitly set then use the type to decide
                    return (this.type == TextFieldType.INPUT);
                }
            }

            set
            {
                this.m_tabEnabledExplicitlySet = true;
                base.tabEnabled = value;
            }
        }
        private bool m_tabEnabledExplicitlySet = false;

        /// <summary>
        /// Initializes a new instance of the TextField class.
        /// </summary>
        public TextField()
        {
            // By default text fields are dynamic but do not support user input
            this.type = TextFieldType.DYNAMIC;

            // TODO bwmott 2013-08-22: Set defaults for all of the properties
            this.background = false;
            this.backgroundColor = 0xffffff;

            this.border = false;
            this.borderColor = 0x000000;

            this.caretIndex = 0;
            this.passwordCharacter = '\u2022';

            this.loadedTextFormat = new TextFormat();
            this.defaultTextFormat = new TextFormat(this.loadedTextFormat.font,
                this.loadedTextFormat.size, this.loadedTextFormat.color,
                this.loadedTextFormat.bold, this.loadedTextFormat.italic,
                this.loadedTextFormat.align, this.loadedTextFormat.leftMargin,
                this.loadedTextFormat.rightMargin, this.loadedTextFormat.indent,
                this.loadedTextFormat.leading);

            this.width = 100;
            this.height = 100;
        }

        internal override bool HitTest(float x, float y, bool shapeFlag, bool evaluateChildren)
        {
            Point localPoint = GlobalToLocal(new Point(x, y));

            return ((localPoint.x >= 0.0f) && (localPoint.x < (this.width)) &&
                (localPoint.y >= 0.0f) &&  (localPoint.y < (this.height)));
        }

        public override object Clone()
        {
            // Return a clone of the Shape with a deep copy of any mutable members
            TextField clone = (TextField)base.Clone();

            return clone;
        }

        private void OnFocusChange(Flare.Events.Event evt)
        {
            FocusEvent focusEvent = evt as FocusEvent;
            this.inFocus = (focusEvent.type == FocusEvent.FOCUS_IN);
        }

        private void OnKeyPress(Flare.Events.Event evt)
        {
            // TODO: support maxChars, multiline, restrict, selectable, selectionBeginIndex,
            //       selectionEndIndex, alwaysShowSelection, scrollH
            // TODO: support keys: start, end, alt+left [word], alt+right, 
            //       shift+left, shift+right, cmd+left, cmd+right, CTRL+A, CTRL+V, CTRL+C, CTRL+X
            KeyboardEvent keyEvent = evt as KeyboardEvent;

            switch (keyEvent.keyCode)
            {
                case Keyboard.KeyCode.LEFT:
                {
                    if (keyEvent.ctrlKey)
                    {
                        this.caretIndex = 0;
                    }
                    else
                    {
                        if (this.caretIndex > 0)
                        {
                            this.caretIndex -= 1;
                        }
                    }
                    break;
                }

                case Keyboard.KeyCode.RIGHT:
                {
                    if (keyEvent.ctrlKey)
                    {
                        this.caretIndex = this.length;
                    }
                    else
                    {
                        if (this.caretIndex < this.length)
                        {
                            this.caretIndex += 1;
                        }
                    }
                    break;
                }

                case Keyboard.KeyCode.BACKSPACE:
                {
                    if (this.caretIndex > 0)
                    {
                        this.m_text = this.m_text.Remove(this.caretIndex - 1, 1);
                        this.caretIndex -= 1;
                        this.m_dirty = true;
                    }
                    break;
                }

                case Keyboard.KeyCode.DELETE:
                {
                    if (this.caretIndex < this.length)
                    {

                        this.m_text = this.m_text.Remove(this.caretIndex, 1);
                        this.m_dirty = true;
                    }
                    break;
                }

                case Keyboard.KeyCode.END:
                {
                    this.caretIndex = this.length;
                    break;
                }

                case Keyboard.KeyCode.HOME:
                {
                    this.caretIndex = 0;
                    break;
                }
                
                case Keyboard.KeyCode.TAB:
                case Keyboard.KeyCode.ENTER:
                case Keyboard.KeyCode.NUMPAD_ENTER:
                {
                    break;
                }

                default:
                {
                    if (keyEvent.charCode != '\0')
                    {
                        this.m_text = this.m_text.Insert(this.caretIndex, "" + keyEvent.charCode);
                        this.caretIndex += 1;
                        this.m_dirty = true;
                    }
                    break;
                }
            }
        }

        private static Material ms_solidMaterial = null;
        private static Material ms_textMaterial = null;

        internal override void Render(RenderContext context)
        {
            if (!this.visible || this.isMask)
            {
                return;
            }
            
            if (this.mask != null)
            {
                context.PushMask(this.mask);
            }
            
            context.SetupMasks();

            // Create rendering materials if they haven't been created
            if (ms_solidMaterial == null)
            {
                Shader solidShader = Shader.Find("Flare/SolidFill");
                Shader textShader = Shader.Find("Flare/Text");

                try
                {
                    ms_solidMaterial = new Material(solidShader);
                    ms_textMaterial = new Material(textShader);
                }
                catch
                {
                    Log.Error(Subsystem.Playback,
                        "Could not find '{0}' shader. Please ensure it is in a Resource folder.",
                        (solidShader == null) ? "Flare/SolidFill" : "Flare/Text");
                }
            }

            float scale = (float)(context.stageTranslationAndScale.d * global::System.Math.Sqrt(
                this.transform.concatenatedMatrix.c * this.transform.concatenatedMatrix.c +
                this.transform.concatenatedMatrix.d * this.transform.concatenatedMatrix.d));

            LayoutTextRecords();

            Matrix matrix = this.transform.concatenatedMatrix;
            ColorTransform cxform = this.transform.concatenatedColorTransform;

            GL.PushMatrix();

            // Setup transform for this shape
            GL.modelview = (Matrix4x4)matrix;

            // Draw background if enabled
            if (this.background)
            {
                ms_solidMaterial.color = cxform.ApplyToColor(this.backgroundColor);
                if (ms_solidMaterial.SetPass(0))
                {
                    GL.Begin(GL.QUADS);
                    GL.Vertex3(-2, -2, 0);
                    GL.Vertex3(width + 3, -2, 0);
                    GL.Vertex3(width + 3, height + 3, 0);
                    GL.Vertex3(-2, height + 3, 0);
                    GL.End();
                }
            }

            // Draw border if enabled
            if (this.border)
            {
                ms_solidMaterial.color = cxform.ApplyToColor(this.borderColor);
                if (ms_solidMaterial.SetPass(0))
                {
                    GL.Begin(GL.LINES);
                    GL.Vertex3(-2, -2, 0);
                    GL.Vertex3(width + 3, -2, 0);
                    GL.Vertex3(width + 3, -2, 0);
                    GL.Vertex3(width + 3, height + 3, 0);
                    GL.Vertex3(width + 3, height + 3, 0);
                    GL.Vertex3(-2, height + 3, 0);
                    GL.Vertex3(-2, height + 3, 0);
                    GL.Vertex3(-2, -2, 0);
                    GL.End();
                }
            }

            // Draw text
            if (!this.m_isHtml)
            {
                GL.modelview = ((Matrix4x4)matrix) * Matrix4x4.TRS(Vector3.zero,
                    Quaternion.identity, new Vector3(1.0f / scale, 1.0f / scale, 1.0f));

                string fontName = this.defaultTextFormat.font;
                string style = Flare.Text.FontStyle.REGULAR;
                int size = (int)(this.defaultTextFormat.size * scale);
                uint color = this.defaultTextFormat.color;

                // Don't look this up every frame...
                DeviceFont font = Font.GetDeviceFont(fontName, style);
                if (this.displayAsPassword)
                {
                    font.unityFont.RequestCharactersInTexture(this.text +
                        this.passwordCharacter, size);
                }
                else
                {
                    font.unityFont.RequestCharactersInTexture(this.text, size);
                }

                float xoff = 0.0f;
                float yoff = font.vertexOffset + (this.defaultTextFormat.size * scale);

                if (ms_textMaterial != null)
                {
                    for (int i = 0; ; ++i)
                    {
                        // Render text cursor every 1/2 second if this is an input text field
                        if ((this.inFocus) && (this.caretIndex == i) &&  
                            (UnityEngine.Time.realtimeSinceStartup % 1.0f > 0.50f) &&
                            (this.type == TextFieldType.INPUT) &&
                            (xoff < this.width * scale))
                        {
                            ms_solidMaterial.color = cxform.ApplyToARGB(color | 0xff000000);
                            if (ms_solidMaterial.SetPass(0))
                            {
                                GL.Begin(GL.QUADS);
                                GL.Vertex3(xoff, 0, 0);
                                GL.Vertex3(xoff, size * 1.15f, 0);
                                GL.Vertex3(xoff + (1.0f * scale), size * 1.15f, 0);
                                GL.Vertex3(xoff + (1.0f * scale), 0, 0);
                                GL.End();
                            }
                        }

                        if (i >= text.Length)
                        {
                            break;
                        }

                        char c = this.displayAsPassword ? this.passwordCharacter : this.text[i]; 
                        
                        CharacterInfo ci;
                        if (font.unityFont.GetCharacterInfo(c, out ci, size))
                        {
                            ms_textMaterial.color = cxform.ApplyToARGB(color | 0xff000000);
                            ms_textMaterial.mainTexture = font.unityFont.material.mainTexture;
                            ms_textMaterial.SetVector("_Clip",
                                new Vector4(0.0f, 0.0f, width * scale, height * scale));
                            ms_textMaterial.SetPass(1);

#if UNITY_5
                            Vector2 v0 = new Vector2(xoff + ci.minX, yoff - ci.maxY);
                            Vector2 v1 = new Vector2(xoff + ci.maxX, yoff - ci.minY);

                            GL.Begin(GL.QUADS);
                            GL.TexCoord2(ci.uvTopLeft.x, ci.uvTopLeft.y);
                            GL.Vertex3(v0.x, v0.y, 0);
                            GL.TexCoord2(ci.uvTopRight.x, ci.uvTopRight.y);
                            GL.Vertex3(v1.x, v0.y, 0);
                            GL.TexCoord2(ci.uvBottomRight.x, ci.uvBottomRight.y);
                            GL.Vertex3(v1.x, v1.y, 0);
                            GL.TexCoord2(ci.uvBottomLeft.x, ci.uvBottomLeft.y);
                            GL.Vertex3(v0.x, v1.y, 0);
                            GL.End();

                            xoff += ci.advance;
#else
                            Vector2 v0 = new Vector2(xoff + ci.vert.xMin, yoff - ci.vert.yMax);
                            Vector2 v1 = new Vector2(xoff + ci.vert.xMax, yoff - ci.vert.yMin);
    
                            Vector2 u0 = new Vector2(ci.uv.xMin, ci.uv.yMin);
                            Vector2 u1 = new Vector2(ci.uv.xMax, ci.uv.yMax);
    
                            GL.Begin(GL.QUADS);
                            if (ci.flipped)
                            {
                                GL.TexCoord2(u0.x, u1.y);
                                GL.Vertex3(v1.x, v0.y, 0);
                                GL.TexCoord2(u0.x, u0.y);
                                GL.Vertex3(v0.x, v0.y, 0);
                                GL.TexCoord2(u1.x, u0.y);
                                GL.Vertex3(v0.x, v1.y, 0);
                                GL.TexCoord2(u1.x, u1.y);
                                GL.Vertex3(v1.x, v1.y, 0);
                            }
                            else
                            {
                                GL.TexCoord2(u1.x, u0.y);
                                GL.Vertex3(v1.x, v0.y, 0);
                                GL.TexCoord2(u0.x, u0.y);
                                GL.Vertex3(v0.x, v0.y, 0);
                                GL.TexCoord2(u0.x, u1.y);
                                GL.Vertex3(v0.x, v1.y, 0);
                                GL.TexCoord2(u1.x, u1.y);
                                GL.Vertex3(v1.x, v1.y, 0);
                            }
                            GL.End();

                            xoff += ci.width;
#endif
                        }
                    }
                }
            }

            // TODO: How to use the Dynamic Text color insteads of the HTML color...

            // Draw text
            if (this.m_isHtml)
            {
                GL.modelview = ((Matrix4x4)matrix) * Matrix4x4.TRS(Vector3.zero,
                    Quaternion.identity, new Vector3(1.0f / scale, 1.0f / scale, 1.0f));

                Point p = new Point();
                foreach (var line in this.lines)
                {
                    p.y += line.height + line.yoffset;
                    p.x = line.xoffset;
                    foreach (var entry in line.characters)
                    {
                        int size = (int)(entry.format.size * scale);
                        UnityEngine.FontStyle style = FontStyle.GetUnityFontStyle(
                            FontStyle.GetStyle(entry.format.bold, entry.format.italic));
                        entry.deviceFont.unityFont.RequestCharactersInTexture("" + entry.character,
                            size, style);

                        CharacterInfo ci;
                        if (entry.deviceFont.unityFont.GetCharacterInfo(entry.character,
                            out ci, size, style))
                        {
                            float yoff = p.y + (entry.deviceFont.vertexOffset / scale);
                            if (entry.deviceFont.metrics != null)
                                yoff -= (1.0f - entry.deviceFont.metrics.ascent) * line.height;
                            float xoff = p.x;
                           
                            xoff = xoff * scale;
                            yoff = yoff * scale;

                            ms_textMaterial.color = cxform.ApplyToARGB(entry.format.color | 0xff000000);
                            ms_textMaterial.mainTexture = entry.deviceFont.unityFont.material.mainTexture;
                            ms_textMaterial.SetVector("_Clip",
                                new Vector4(0.0f, 0.0f, width * scale, height * scale));
                            ms_textMaterial.SetPass(1);

#if UNITY_5
                            Vector2 v0 = new Vector2(xoff + ci.minX, yoff - ci.maxY);
                            Vector2 v1 = new Vector2(xoff + ci.maxX, yoff - ci.minY);
                            
                            GL.Begin(GL.QUADS);
                            GL.TexCoord2(ci.uvTopLeft.x, ci.uvTopLeft.y);
                            GL.Vertex3(v0.x, v0.y, 0);
                            GL.TexCoord2(ci.uvTopRight.x, ci.uvTopRight.y);
                            GL.Vertex3(v1.x, v0.y, 0);
                            GL.TexCoord2(ci.uvBottomRight.x, ci.uvBottomRight.y);
                            GL.Vertex3(v1.x, v1.y, 0);
                            GL.TexCoord2(ci.uvBottomLeft.x, ci.uvBottomLeft.y);
                            GL.Vertex3(v0.x, v1.y, 0);
                            GL.End();
                            
                            p.x += entry.width;
#else
                            Vector2 v0 = new Vector2(xoff + ci.vert.xMin, yoff - ci.vert.yMax);
                            Vector2 v1 = new Vector2(xoff + ci.vert.xMax, yoff - ci.vert.yMin);

                            Vector2 u0 = new Vector2(ci.uv.xMin, ci.uv.yMin);
                            Vector2 u1 = new Vector2(ci.uv.xMax, ci.uv.yMax);
                          
                            GL.Begin(GL.QUADS);
                            GL.TexCoord(ci.flipped ? new Vector3(u0.x, u1.y, 0) : new Vector3(u1.x, u0.y, 0));
                            GL.Vertex3(v1.x, v0.y, 0);
                            GL.TexCoord(new Vector3(u0.x, u0.y, 0));
                            GL.Vertex3(v0.x, v0.y, 0);
                            GL.TexCoord(ci.flipped ? new Vector3(u1.x, u0.y, 0) : new Vector3(u0.x, u1.y, 0));
                            GL.Vertex3(v0.x, v1.y, 0);
                            GL.TexCoord(new Vector3(u1.x, u1.y, 0));
                            GL.Vertex3(v1.x, v1.y, 0);
                            GL.End();

                            p.x += entry.width;
#endif
                        }
                    }
                }
            }

            GL.PopMatrix();

            if (this.mask != null)
            {
                context.PopMask();
            }
        }

        internal override void RenderAsMask(RenderContext context)
        {
            // TODO bwmott 2014-06-10: Need to render text using mask shader
        }

        private bool m_dirty = true;

        private void LayoutTextRecords()
        {
            if (!this.m_dirty)
            {
                return;
            }
            this.m_dirty = false;

            if (!this.m_isHtml)
            {
                return;
            }
            
            // TODO bwmott 2013-09-04: Handle autoSize?
            this.lines = new List<Line>();

            bool startingLine = true;
            Line currentLine = new Line();
            foreach (TextRecord record in this.textRecords)
            {
                int size = (int)record.format.size;

                UnityEngine.FontStyle style = FontStyle.GetUnityFontStyle(
                    FontStyle.GetStyle(record.format.bold, record.format.italic));

                // Ensure characters are in font texture
                record.deviceFont.unityFont.RequestCharactersInTexture(record.text, size, style);

                foreach (char c in record.text)
                {
                    if (startingLine)
                    {
                        currentLine.align = record.format.align;
                        currentLine.leftMargin = record.format.leftMargin;
                        currentLine.rightMargin = record.format.rightMargin;
                        currentLine.indent = record.format.indent;
                        currentLine.leading = record.format.leading;
                        currentLine.size = record.format.size;
                    }

                    if (c == '\n')
                    {
                        currentLine.isParagraphEnd = true;
                        this.lines.Add(currentLine);
                        currentLine = new Line();
                        startingLine = true;
                    }
                    else
                    {
                        CharacterInfo ci;
                        if (record.deviceFont.unityFont.GetCharacterInfo(c, out ci, size, style))
                        {
#if UNITY_5
                            float w = ci.advance;
#else
                            float w = ci.width;
#endif
                            currentLine.characters.Add(new Character(c, record.format, ci, record.deviceFont,
                                w + record.format.letterSpacing));
                        }
                    }
                }
            }
            lines.Add(currentLine);


            float yoffset = 0.0f;

            for (int i = 0; i < this.lines.Count; ++i)
            {
                var line = this.lines[i];
                if (line.characters.Count > 0)
                {
                    // Paragraph level formatting information
                    string align = line.align;
                    float leftMargin = line.leftMargin;
                    float rightMargin = line.rightMargin;
                    float indent = line.indent;

                    float availableWidth = width - (line.isParagraphStart ? (leftMargin + indent + rightMargin)
                        : (leftMargin + rightMargin));
                    if (availableWidth < 0.0f)
                    {
                        Debug.Log ("ERROR: width is too small!!!!");
                    }

                    float w = 0.0f;
                    for (int j = 0; j < line.characters.Count; ++j)
                    {
                        var entry = line.characters[j];
                        if (this.wordWrap && (availableWidth < (w + entry.width)))
                        {
                            int breakIndex = j;
                            for (int k = j; k >= 0; --k)
                            {
                                if (line.characters[k].character == ' ')
                                {
                                    breakIndex = k;
                                    break;
                                }
                            }

                            Line rest = line.SplitAt(breakIndex);
                            this.lines.Insert(i + 1, rest);
    // TODO bwmott 2013-09-05: This should only be done if we're splitting at a space
    rest.characters.RemoveAt(0);
                        }
                        else
                        {
                            w += entry.width;
                        }
                    }

                    w = 0.0f;
                    foreach (var entry in line.characters)
                    {
                        w += entry.width;
                    }

                    if (align == TextFormatAlign.RIGHT)
                    {
                        line.xoffset = leftMargin + availableWidth - w;

                    }
                    else if (align == TextFormatAlign.CENTER)
                    {
                        line.xoffset = leftMargin + ((availableWidth - w) / 2.0f);
                    }
                    else
                    {
                        line.xoffset = leftMargin;

                        // Don't do this on the last line of a paragraph
                        if (this.wordWrap && (align == TextFormatAlign.JUSTIFY) && !line.isParagraphEnd)
                        {
                            int count = 0;
                            for (int j = 0; j < line.characters.Count; ++j)
                            {
                                if (line.characters[j].character == ' ')
                                {
                                    count += 1;
                                }
                            }
                            if (count > 0)
                            {
                                float space = (availableWidth - w) / count;
                                for (int j = 0; j < line.characters.Count; ++j)
                                {
                                    if (line.characters[j].character == ' ')
                                    {
                                        line.characters[j].width += space;
                                    }
                                }
                            }
                        }
                    }

                    if (line.isParagraphStart)
                    {
                        line.xoffset += line.indent;
                    }


                    line.yoffset = yoffset;

                    yoffset = 0.0f;
                }
                else
                {
                    yoffset = line.size;
                }

                // TODO bwmott 2013-09-05: What is the correct conversion for leading to pixels?
                yoffset += line.leading * 96.0f / 72.0f;
            }
        }

        //--------------------------------------------------------------------------------

        private class TextRecord
        {
            public TextFormat format { get; set; }
            public string text { get; set; }
            public DeviceFont deviceFont { get; private set; }

            public TextRecord(TextFormat format, string text)
            {
                this.format = format;
                this.text = text;

                // Look up the device font with the given name or a fallback font if it doesn't exist
                this.deviceFont = Font.GetDeviceFont(this.format.font,
                    FontStyle.GetStyle(this.format.bold, this.format.italic), true);
            }
        }

        //--------------------------------------------------------------------------------

        private class Line
        {
            public List<Character> characters { get; set; }
            public bool isParagraphStart { get; set; }
            public bool isParagraphEnd { get; set; }

            public float xoffset { get; set; }
            public float yoffset { get; set; }

            // Paragraph formatting
            public string align { get; set; }
            public float leftMargin { get; set; }
            public float rightMargin { get; set; }
            public float indent { get; set; }
            public float leading { get; set; }
            public float size { get; set; }

            public float height
            {
                get
                {
                    float maxHeight = 0.0f;
                    foreach (var entry in this.characters)
                    {
                        if (entry.format.size > maxHeight)
                        {
                            maxHeight = entry.format.size;
                        }
                    }

                    return maxHeight;
                }
            }

            public Line()
            {
                this.characters = new List<Character>();
                this.isParagraphStart = true;
                this.isParagraphEnd = false;
                this.xoffset = 0.0f;
                this.yoffset = 0.0f;
            }

            public Line SplitAt(int index)
            {
                Line rest = new Line();
                rest.isParagraphStart = false;
                rest.isParagraphEnd = this.isParagraphEnd;
                this.isParagraphEnd = false;

                for (int i = index; i <  this.characters.Count; ++i)
                {
                    rest.characters.Add(this.characters[i]);
                }
                this.characters.RemoveRange(index, this.characters.Count - index);

                rest.align = this.align;
                rest.leftMargin = this.leftMargin;
                rest.rightMargin = this.rightMargin;
                rest.indent = this.indent;
                rest.leading = this.leading;
                rest.size = this.size;

                return rest;
            }
        }

        //--------------------------------------------------------------------------------

        private class Character
        {
            public char character { get; private set; }
            public TextFormat format { get; private set; }
            public CharacterInfo characterInfo { get; private set; }
            public DeviceFont deviceFont { get; private set; }
            public float width { get; set; }

            public Character(char character, TextFormat format, CharacterInfo characterInfo,
                DeviceFont deviceFont, float width)
            {
                this.character = character;
                this.format = format;
                this.characterInfo = characterInfo;
                this.deviceFont = deviceFont;
                this.width = width;
            }
        }

        //--------------------------------------------------------------------------------

        private TextRecord[] textRecords { get; set; }
        private List<Line> lines { get; set; }

        private void ParseTextRecords()
        {
            List<TextRecord> records = new List<TextRecord>();

            // TODO: test with mixed-case XML tags and attributes

            this.m_dirty = true;
            
            if (!this.m_isHtml)
            {
                foreach (var s in this.text.Split('\n'))
                {
                    // Plain text data, so we only need to create a single text record
                    records.Add(new TextRecord(this.defaultTextFormat, s + "\n"));
                }
            }
            else
            {
                Stack<TextFormat> formatStack = new Stack<TextFormat>();

                // TODO: default? or "loaded"?
                formatStack.Push(new TextFormat(this.loadedTextFormat));

                XmlReaderSettings settings = new XmlReaderSettings();
                settings.ConformanceLevel = ConformanceLevel.Fragment;

                string html = this.m_htmlText.Replace("&nbsp;", "&#160;");

                // TODO: need to add some error checking
                using (XmlReader reader = XmlReader.Create(
                    new global::System.IO.StringReader(html), settings))
                {
                    bool paragraphContainedContent = false;

                    while (reader.Read())
                    {
                        // Handle start elements
                        if (reader.NodeType == XmlNodeType.Element)
                        {
                            if (reader.Name.Equals("p"))
                            {
                                TextFormat format = new TextFormat(formatStack.Peek());
                                if (reader.HasAttributes)
                                {
                                    while (reader.MoveToNextAttribute())
                                    {
                                        if (reader.Name.Equals("align"))
                                        {
                                            format.align = reader.Value;
                                        }
                                    }
                                }
                                formatStack.Push(format);
                                paragraphContainedContent = false;
                            }
                            else if (reader.Name.Equals("b"))
                            {
                                TextFormat format = new TextFormat(formatStack.Peek());
                                format.bold = true;
                                formatStack.Push(format);
                            }
                            else if (reader.Name.Equals("i"))
                            {
                                TextFormat format = new TextFormat(formatStack.Peek());
                                format.italic = true;
                                formatStack.Push(format);
                            }
                            else if (reader.Name.Equals("font"))
                            {
                                TextFormat format = new TextFormat(formatStack.Peek());
                                while (reader.HasAttributes && reader.MoveToNextAttribute())
                                {
                                    if (reader.Name.Equals("face"))
                                    {
                                        format.font = reader.Value;
                                    }
                                    else if (reader.Name.Equals("size"))
                                    {
                                        // TODO: Relative sizes
                                        format.size = float.Parse(reader.Value);
                                    }
                                    else if (reader.Name.Equals("color"))
                                    {
                                        format.color = uint.Parse(reader.Value.Substring(1),
                                            global::System.Globalization.NumberStyles.HexNumber);
                                    }
                                    else if (reader.Name.Equals("letterSpacing"))
                                    {
                                        format.letterSpacing = float.Parse(reader.Value);
                                    }
                                }
                                formatStack.Push(format);
                            }
                        }
                        // Handle end elements
                        else if (reader.NodeType == XmlNodeType.EndElement)
                        {
                            if (reader.Name.Equals("p"))
                            {
                                if (paragraphContainedContent)
                                {
                                    records[records.Count - 1].text += "\n";
                                }
                                else
                                {
                                    records.Add(new TextRecord(formatStack.Peek(), "\n"));
                                }
                                formatStack.Pop();
                            }
                            else if (reader.Name.Equals("b"))
                            {
                                formatStack.Pop();
                            }
                            else if (reader.Name.Equals("i"))
                            {
                                formatStack.Pop();
                            }
                            else if (reader.Name.Equals("font"))
                            {
                                formatStack.Pop();
                            }
                        }
                        else if (reader.NodeType == XmlNodeType.Text)
                        {
                            records.Add(new TextRecord(formatStack.Peek(), reader.Value));
                            paragraphContainedContent = true;
                        }
                    }
                }
            }

            /*
            foreach (var r in records)
            {
                Debug.Log("[" + r.text + "] " + r.format);
            }
            */

            this.textRecords = records.ToArray();
        }
    }
}